"use strict";

const mysql = require("mysql");
const dataHelp = require("./util/data.helper.js");
const whereHelp = require("./util/whereClause.helper.js");
const assert = require("assert");

//define class§
class Xsql {
  constructor(sqlConfig, pool) {
    //define this variables
    this.sqlConfig = {};
    this.pool = {};
    this.metaDb = {};
    this.metaDb.tables = {};

    this.sqlConfig = sqlConfig;
    this.pool = pool;
  }

  /**************** START : Cache functions ****************/
  init(cbk) {
    this.dbCacheInitAsync((err, results) => {
      cbk(err, results);
    });
  }

  dbCacheInitAsync(cbk) {
    let self = this;

    self.pool.query(
      dataHelp.getSchemaQuery(),
      [this.sqlConfig.database],
      (err, results) => {
        if (err) {
          console.log("Cache init failed during database reading");
          console.log(err, results);
          cbk(err, results);
        } else {
          for (var i = 0; i < results.length; ++i) {
            let keys = Object.keys(results[i]);

            for (var j = 0; j < keys.length; ++j) {
              let value = results[i][keys[j]];

              results[i][keys[j].toLowerCase()] = value;

              //console.log(value);
            }
          }

          self.iterateToCacheTables(results);
          self.iterateToCacheTablePks(results);
          self.iterateToCacheTableColumns(results);
          self.iterateToCacheTableFks(results);

          // osx mysql server has limitations related to open_tables
          self.pool.query("FLUSH TABLES", [], (err, results) => {
            cbk(null, null);
          });
        }
      }
    );
  }

  iterateToCacheTables(schemaResults) {
    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];

      let tableName = schemaRow["table_name"];

      if (!(tableName in this.metaDb.tables)) {
        this.metaDb.tables[tableName] = {};
        this.metaDb.tables[tableName]["primaryKeys"] = [];
        this.metaDb.tables[tableName]["foreignKeys"] = [];
        this.metaDb.tables[tableName]["columns"] = [];
        this.metaDb.tables[tableName]["indicies"] = [];
        this.metaDb.tables[tableName]["isView"] = schemaRow["isView"];
      }
    }
  }

  iterateToCacheTableColumns(schemaResults) {
    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];
      let tableName = schemaRow["table_name"];
      let col = {};
      col["column_name"] = schemaRow["column_name"];
      col["ordinal_position"] = schemaRow["ordinal_position"];
      col["column_key"] = schemaRow["column_key"];
      col["data_type"] = schemaRow["data_type"];
      col["column_type"] = schemaRow["column_type"];

      dataHelp.findOrInsertObjectArrayByKey(
        col,
        "column_name",
        this.metaDb.tables[tableName]["columns"]
      );
    }
  }

  iterateToCacheTablePks(schemaResults) {
    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];
      let tableName = schemaRow["table_name"];

      if (schemaRow["column_key"] === "PRI") {
        let pk = {};
        pk["column_name"] = schemaRow["column_name"];
        pk["ordinal_position"] = schemaRow["ordinal_position"];
        pk["column_key"] = schemaRow["column_key"];
        pk["data_type"] = schemaRow["data_type"];
        pk["column_type"] = schemaRow["column_type"];

        dataHelp.findOrInsertObjectArrayByKey(
          pk,
          "column_name",
          this.metaDb.tables[tableName]["primaryKeys"]
        );
      }
    }
  }

  iterateToCacheTableFks(schemaResults) {
    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];
      let tableName = schemaRow["table_name"];

      if (schemaRow["referenced_table_name"]) {
        let fk = {};

        fk["column_name"] = schemaRow["column_name"];
        fk["table_name"] = schemaRow["table_name"];
        fk["referenced_table_name"] = schemaRow["referenced_table_name"];
        fk["referenced_column_name"] = schemaRow["referenced_column_name"];
        fk["data_type"] = schemaRow["data_type"];
        fk["column_type"] = schemaRow["column_type"];

        dataHelp.findOrInsertObjectArrayByKey(
          fk,
          "column_name",
          this.metaDb.tables[tableName]["foreignKeys"]
        );

        //console.log(fk['referenced_table_name'],fk['referenced_column_name'],tableName, schemaRow['column_name'], this.metaDb.tables[tableName]['foreignKeys'].length)
      }
    }
  }

  /**************** END : Cache functions ****************/

  exec(query, params) {
    let _this = this;
    return new Promise(function(resolve, reject) {
      //console.log('mysql>', query, params);
      _this.pool.query(query, params, function(error, rows, _fields) {
        if (error) {
          console.log("mysql> ", error);
          return reject(error);
        }
        return resolve(rows);
      });
    });
  }

  typeOfColumn(Type) {
    //TODO: Im sure there are more types to handle here
    const strTypes = [
      "varchar",
      "text",
      "char",
      "tinytext",
      "mediumtext",
      "longtext",
      "blob",
      "mediumblob",
      "longblob"
    ];
    const intTypes = [
      "int",
      "long",
      "smallint",
      "mediumint",
      "bigint",
      "tinyint"
    ];
    const flatTypes = ["float", "double", "decimal"];
    const dateTypes = ["date", "datetime", "timestamp", "time", "year"];

    if (dataHelp.getType(Type, strTypes)) {
      return "string";
    } else if (dataHelp.getType(Type, intTypes)) {
      return "int";
    } else if (dataHelp.getType(Type, flatTypes)) {
      return "float";
    } else if (dataHelp.getType(Type, dateTypes)) {
      return "date";
    } else {
      return "unknown";
    }
  }

  isTypeOfColumnNumber(Type) {
    //console.log(Type, this.typeOfColumn(Type));
    return (
      "int" === this.typeOfColumn(Type) || "float" === this.typeOfColumn(Type)
    );
  }

  getLimitClause(reqParams) {
    //defaults
    reqParams._index = 0;
    reqParams._len = 1000;

    if ("_size" in reqParams) {
      if (parseInt(reqParams._size) > 0 && parseInt(reqParams._size) <= 10000) {
        reqParams._len = parseInt(reqParams._size);
      } else if (parseInt(reqParams._size) > 10000) {
        reqParams._len = 10000;
      }
    }

    if ("_p" in reqParams && parseInt(reqParams._p) > 0) {
      reqParams._index = parseInt(reqParams._p) * reqParams._len;
    }

    //console.log(reqParams._index, reqParams._len);

    return [reqParams._index, reqParams._len];
  }

  prepareBulkInsert(tableName, objectArray, queryParamsObj) {
    if (tableName in this.metaDb.tables && objectArray) {
      let insObj = objectArray[0];

      // goal => insert into ?? (?,?..?) values ? [tablName, col1,col2...coln,[[ObjValues_1],[ObjValues_2],...[ObjValues_N]]
      queryParamsObj.query = " INSERT INTO ?? ( ";
      queryParamsObj.params.push(tableName);

      let cols = [];
      let colPresent = false;

      /**************** START : prepare column names to be inserted ****************/
      // iterate over all column in table and have only ones existing in objects to be inserted
      for (
        var i = 0;
        i < this.metaDb.tables[tableName]["columns"].length;
        ++i
      ) {
        let colName = this.metaDb.tables[tableName]["columns"][i][
          "column_name"
        ];

        if (colName in insObj) {
          if (colPresent) {
            queryParamsObj.query += ",";
          }

          queryParamsObj.query += colName;

          colPresent = true;
        }

        cols.push(colName);

        //console.log('> > ', queryParamsObj.query);
      }

      queryParamsObj.query += " ) values ?";
      /**************** END : prepare column names to be inserted ****************/

      /**************** START : prepare value object in prepared statement ****************/
      // iterate over sent object array
      let arrOfArr = [];
      for (var i = 0; i < objectArray.length; ++i) {
        let arrValues = [];
        for (var j = 0; j < cols.length; ++j) {
          if (cols[j] in objectArray[i])
            arrValues.push(objectArray[i][cols[j]]);
        }
        arrOfArr.push(arrValues);
      }
      queryParamsObj.params.push(arrOfArr);
      /**************** END : prepare value object in prepared statement ****************/
    }
  }

  getGroupByClause(_groupby, tableName, queryParamsObj) {
    if (_groupby) {
      queryParamsObj.query += " group by " + _groupby + " ";
      return _groupby;
    }
  }

  getHavingClause(_having, tableName, queryParamsObj) {
    if (_having) {
      let whereClauseObj = whereHelp.getConditionClause(_having, "having");

      if (whereClauseObj.err === 0) {
        queryParamsObj.query =
          queryParamsObj.query + " having " + whereClauseObj.query;
        queryParamsObj.params = queryParamsObj.params.concat(
          whereClauseObj.params
        );
      }

      //console.log('> > > after where clause filling up:', queryParamsObj.query, queryParamsObj.params);
    }
  }

  getWhereClause(queryparams, tableName, queryParamsObj, appendToWhere) {
    if (queryparams) {
      let whereClauseObj = whereHelp.getConditionClause(queryparams);

      if (whereClauseObj.err === 0) {
        queryParamsObj.query =
          queryParamsObj.query + appendToWhere + whereClauseObj.query;
        queryParamsObj.params = queryParamsObj.params.concat(
          whereClauseObj.params
        );
      }
    }
  }

  getOrderByClause(queryparams, tableName, queryParamsObj) {
    if (queryparams._sort) {
      queryParamsObj.query += " ORDER BY ";

      let orderByCols = queryparams._sort.split(",");

      for (let i = 0; i < orderByCols.length; ++i) {
        if (i) {
          queryParamsObj.query += ", ";
        }
        if (orderByCols[i][0] === "-") {
          let len = orderByCols[i].length;
          queryParamsObj.query += " ?? DESC";
          queryParamsObj.params.push(orderByCols[i].substring(1, len));
        } else {
          queryParamsObj.query += " ?? ASC";
          queryParamsObj.params.push(orderByCols[i]);
        }
      }
    }
  }

  getColumnsForSelectStmtWithGrpBy(reqQueryParams, tableName, queryParamsObj) {
    let grpByCols = reqQueryParams._groupby.split(",");

    for (var i = 0; i < grpByCols.length; ++i) {
      if (i) {
        queryParamsObj.query += ",";
      }
      queryParamsObj.query += " ??";
      queryParamsObj.params.push(grpByCols[i]);
    }

    queryParamsObj.query += ",count(1) as _count ";
  }

  getColumnsForSelectStmt(tableName, reqQueryParams, queryParamsObj) {
    let table = this.metaDb.tables[tableName];
    let cols = [];
    let _fieldsInQuery = [];
    let removeFieldsObj = {};

    // populate _fields array from query params
    if ("_fields" in reqQueryParams) {
      _fieldsInQuery = reqQueryParams["_fields"].split(",");
    } else {
      queryParamsObj.query += " * ";
      return " * ";
    }

    // get column name in _fields and mark column name which start with '-'
    for (let i = 0; i < _fieldsInQuery.length; ++i) {
      if (_fieldsInQuery[i][0] === "-") {
        removeFieldsObj[
          _fieldsInQuery[i].substring(1, _fieldsInQuery[i].length)
        ] = 1;
      } else {
        cols.push(_fieldsInQuery[i]);
      }
    }

    if (!cols.length) {
      // for each column in table - add only which are not in removeFieldsObj
      for (let i = 0; i < table["columns"].length; ++i) {
        if (!(table["columns"][i]["column_name"] in removeFieldsObj)) {
          cols.push(table["columns"][i]["column_name"]);
        }
      }
    } else {
      cols = this.removeUnknownColumns(cols, tableName);
    }

    for (var i = 0; i < cols.length; ++i) {
      if (i) {
        queryParamsObj.query += ",";
      }
      queryParamsObj.query += "??";
      queryParamsObj.params.push(cols[i]);
    }

    return cols.join(",");
  }

  removeUnknownColumns(inputColumns, tableName) {
    let cols = inputColumns;
    let unknown_cols_in_input = [];
    let shadowCols = [];
    let tableColumns = this.metaDb.tables[tableName]["columns"];

    // find unknown fields if any
    for (var j = 0; j < cols.length; ++j) {
      let found = 0;

      for (var i = 0; i < tableColumns.length; ++i) {
        if (tableColumns[i]["column_name"] === cols[j]) {
          found = 1;
          break;
        }
      }

      if (!found) {
        unknown_cols_in_input.push(j);
      }
    }

    // if there are unknown fields - remove and ignore 'em
    if (unknown_cols_in_input.length) {
      for (var i = 0; i < cols.length; ++i) {
        if (unknown_cols_in_input.indexOf(i) === -1) {
          shadowCols.push(cols[i]);
        }
      }

      cols = [];
      cols = shadowCols;
    }

    return cols;
  }

  getPrimaryKeyName(tableName) {
    let pk = null;
    if (tableName in this.metaDb.tables) {
      pk = this.metaDb.tables[tableName].primaryKeys[0]["column_name"];
    }
    return pk;
  }

  getPrimaryKeyWhereClause(tableName, pksValues) {
    let whereClause = "";
    let whereCol = "";
    let whereValue = "";
    let pks = [];

    if (tableName in this.metaDb.tables) {
      pks = this.metaDb.tables[tableName].primaryKeys;
    } else {
      return null;
    }

    // number of primary keys in table and one sent should be same
    if (pksValues.length !== pks.length) {
      return null;
    }

    // get a where clause out of the above columnNames and their values
    for (let i = 0; i < pks.length; ++i) {
      let type = dataHelp.getColumnType(pks[i]);

      whereCol = pks[i]["column_name"];

      if (type === "string") {
        whereValue = mysql.escape(pksValues[i]);
      } else if (type === "int") {
        whereValue = parseInt(pksValues[i]);
      } else if (type === "float") {
        whereValue = parseFloat(pksValues[i]);
      } else if (type === "date") {
        whereValue = Date(pksValues[i]);
      } else {
        console.error(pks[i]);
        assert(false, "Unhandled type of primary key");
      }

      if (i) {
        whereClause += " and ";
      }

      whereClause += whereCol + " = " + whereValue;
    }

    return whereClause;
  }

  getForeignKeyWhereClause(parentTable, parentId, childTable) {
    let whereValue = "";

    //get all foreign keys of child table
    let fks = this.metaDb.tables[childTable].foreignKeys;
    let fk = dataHelp.findObjectInArrayByKey(
      "referenced_table_name",
      parentTable,
      fks
    );
    let whereCol = fk["column_name"];
    let colType = dataHelp.getColumnType(fk);

    if (colType === "string") {
      whereValue = mysql.escape(parentId);
    } else if (colType === "int") {
      whereValue = mysql.escape(parseInt(parentId));
    } else if (colType === "float") {
      whereValue = mysql.escape(parseFloat(parentId));
    } else if (colType === "date") {
      whereValue = mysql.escape(Date(parentId));
    } else {
      console.error(pks[i]);
      assert(false, "Unhandled column type in foreign key handling");
    }

    return whereCol + " = " + whereValue;
  }

  prepareRoute(internal, httpType, apiPrefix, urlRoute, routeType) {
    let route = {};
    route["httpType"] = httpType;
    route["routeUrl"] = apiPrefix + urlRoute;
    if (internal) {
      route["routeType"] = routeType;
    }
    return route;
  }

  getSchemaRoutes(internal, apiPrefix) {
    let schemaRoutes = [];

    for (var tableName in this.metaDb.tables) {
      if (tableName in this.sqlConfig.ignoreTables) {
        //console.log('ignore table', tableName);
      } else {
        let routes = [];
        let tableObj = {};
        let table = this.metaDb.tables[tableName];
        let isView = this.metaDb.tables[tableName]["isView"];

        tableObj["resource"] = tableName;

        // order of routes is important for express routing - DO NOT CHANGE ORDER
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/describe",
            "describe"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/count",
            "count"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/groupby",
            "groupby"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/distinct",
            "distinct"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/ugroupby",
            "ugroupby"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/chart",
            "chart"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/aggregate",
            "aggregate"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/findOne",
            "findOne"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/autoChart",
            "autoChart"
          )
        );

        if (!isView && !this.sqlConfig.readOnly) {
          routes.push(
            this.prepareRoute(internal, "post", apiPrefix, tableName, "create")
          );
        }
        routes.push(
          this.prepareRoute(internal, "get", apiPrefix, tableName, "list")
        );

        if (!isView && !this.sqlConfig.readOnly) {
          routes.push(
            this.prepareRoute(
              internal,
              "post",
              apiPrefix,
              tableName + "/bulk",
              "bulkInsert"
            )
          );
          routes.push(
            this.prepareRoute(
              internal,
              "delete",
              apiPrefix,
              tableName + "/bulk",
              "bulkDelete"
            )
          );
        }
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/bulk",
            "bulkRead"
          )
        );

        if (!isView && !this.sqlConfig.readOnly) {
          routes.push(
            this.prepareRoute(internal, "put", apiPrefix, tableName, "update")
          );
          routes.push(
            this.prepareRoute(
              internal,
              "patch",
              apiPrefix,
              tableName + "/:id",
              "patch"
            )
          );
          routes.push(
            this.prepareRoute(
              internal,
              "delete",
              apiPrefix,
              tableName + "/:id",
              "delete"
            )
          );
        }

        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/:id",
            "read"
          )
        );
        routes.push(
          this.prepareRoute(
            internal,
            "get",
            apiPrefix,
            tableName + "/:id/exists",
            "exists"
          )
        );

        for (var j = 0; j < table["foreignKeys"].length; ++j) {
          let fk = table["foreignKeys"][j];

          if (fk["referenced_table_name"] in this.sqlConfig.ignoreTables) {
            //console.log('ignore table',fk['referenced_table_name']);
          } else {
            routes.push(
              this.prepareRoute(
                internal,
                "get",
                apiPrefix,
                fk["referenced_table_name"] + "/:id/" + fk["table_name"],
                "relational"
              )
            );
          }
        }

        tableObj["routes"] = routes;

        schemaRoutes.push(tableObj);
      }
    }

    return schemaRoutes;
  }

  getJoinType(joinInQueryParams) {
    //console.log('joinInQueryParams',joinInQueryParams);

    switch (joinInQueryParams) {
      case "_lj":
        return " left join ";
        break;

      case "_rj":
        return " right join ";
        break;

      // case '_fj':
      //   return ' full join '
      //   break;

      case "_ij":
        return " inner join ";
        break;

      case "_j":
        return " join ";
        break;
    }

    return " join ";
  }

  globalRoutesPrint(apiPrefix) {
    let r = [];

    r.push(apiPrefix + "tables");
    r.push(apiPrefix + "xjoin");

    if (this.sqlConfig.dynamic) {
      r.push(apiPrefix + "dynamic");
      r.push("/upload");
      r.push("/uploads");
      r.push("/download");
    }

    return r;
  }

  getChartQueryAndParamsFromStepPair(
    tableName,
    columnName,
    stepArray,
    isRange = false
  ) {
    let obj = {};

    obj.query = "";
    obj.params = [];

    //console.log('getChartQueryAndParamsFromStepArray',isRange);

    //select ? as ??, count(*) as _count from ?? where ?? between ? and ?

    if (
      stepArray.length &&
      stepArray.length >= 2 &&
      stepArray.length % 2 === 0
    ) {
      for (
        let i = 0;
        i < stepArray.length && stepArray.length >= 2;
        i = i + 2
      ) {
        obj.query = obj.query + dataHelp.getChartQuery();

        if (i + 2 < stepArray.length) {
          obj.query = obj.query + " union ";
        }

        obj.params.push(stepArray[i] + " to " + stepArray[i + 1]);
        obj.params.push(columnName);
        obj.params.push(tableName);
        obj.params.push(columnName);
        obj.params.push(stepArray[i]);
        obj.params.push(stepArray[i + 1]);
      }
    }

    //console.log('step spread query', obj);

    return obj;
  }

  getChartQueryAndParamsFromStepArray(
    tableName,
    columnName,
    stepArray,
    isRange = false
  ) {
    let obj = {};

    obj.query = "";
    obj.params = [];

    //console.log('getChartQueryAndParamsFromStepArray',isRange);

    if (stepArray.length && stepArray.length >= 2) {
      for (let i = 0; i < stepArray.length - 1; i = i + 1) {
        obj.query = obj.query + dataHelp.getChartQuery();
        if (i + 2 < stepArray.length) {
          obj.query = obj.query + " union ";
        }

        if (i && isRange === false) {
          stepArray[i] = stepArray[i] + 1;
        }

        if (isRange === false) {
          obj.params.push(stepArray[i] + " to " + stepArray[i + 1]);
        } else {
          obj.params.push(stepArray[0] + " to " + stepArray[i + 1]);
        }

        obj.params.push(columnName);
        obj.params.push(tableName);
        obj.params.push(columnName);

        if (isRange === false) {
          obj.params.push(stepArray[i]);
          obj.params.push(stepArray[i + 1]);
        } else {
          obj.params.push(stepArray[0]);
          obj.params.push(stepArray[i + 1]);
        }
      }
    }

    //console.log('step spread query', obj);

    return obj;
  }

  getChartQueryAndParamsFromMinMaxStddev(
    tableName,
    columnName,
    min,
    max,
    stddev,
    isRange = false
  ) {
    let stepArray = dataHelp.getStepArray(min, max, stddev);

    //console.log('steparray', stepArray);

    let obj = this.getChartQueryAndParamsFromStepArray(
      tableName,
      columnName,
      stepArray,
      isRange
    );

    //console.log('steparray', obj);

    return obj;
  }

  getChartQueryAndParamsFromMinMaxStep(
    tableName,
    columnName,
    min,
    max,
    step,
    isRange = false
  ) {
    let stepArray = dataHelp.getStepArraySimple(min, max, step);

    //console.log('steparray', stepArray);

    let obj = this.getChartQueryAndParamsFromStepArray(
      tableName,
      columnName,
      stepArray,
      isRange
    );

    //console.log('steparray', obj);

    return obj;
  }

  _getGrpByHavingOrderBy(req, tableName, queryParamsObj, listType) {
    /**************** add group by ****************/
    this.getGroupByClause(
      req.query._groupby,
      req.app.locals._tableName,
      queryParamsObj
    );

    /**************** add having ****************/
    this.getHavingClause(
      req.query._having,
      req.app.locals._tableName,
      queryParamsObj
    );

    /**************** add order clause ****************/
    this.getOrderByClause(req.query, req.app.locals._tableName, queryParamsObj);

    /**************** add limit clause ****************/
    if (listType === 2) {
      //nested
      queryParamsObj.query += " limit 1 ";
    } else {
      queryParamsObj.query += " limit ?,? ";
      queryParamsObj.params = queryParamsObj.params.concat(
        this.getLimitClause(req.query)
      );
    }
  }

  /**
   *
   * @param req
   * @param res
   * @param queryParamsObj : {query, params}
   * @param listType : 0:list, 1:nested, 2:findOne, 3:bulkRead, 4:distinct, 5:xjoin
   *
   * Updates query, params for query of type listType
   */
  prepareListQuery(req, res, queryParamsObj, listType = 0) {
    queryParamsObj.query = "select ";
    queryParamsObj.params = [];

    if (listType === 4) {
      //list type distinct
      queryParamsObj.query += " distinct ";
    }

    /**************** select columns ****************/
    if (req.query._groupby) {
      this.getColumnsForSelectStmtWithGrpBy(
        req.query,
        req.app.locals._tableName,
        queryParamsObj
      );
    } else {
      this.getColumnsForSelectStmt(
        req.app.locals._tableName,
        req.query,
        queryParamsObj
      );
    }

    /**************** add tableName ****************/
    queryParamsObj.query += " from ?? ";

    if (listType === 1) {
      //nested list

      req.app.locals._tableName = req.app.locals._childTable;

      queryParamsObj.params.push(req.app.locals._childTable);

      queryParamsObj.query += " where ";

      /**************** add where foreign key ****************/
      let whereClause = this.getForeignKeyWhereClause(
        req.app.locals._parentTable,
        req.params.id,
        req.app.locals._childTable
      );

      if (!whereClause) {
        return res.status(400).send({
          error:
            "Table is made of composite primary keys - all keys were not in input"
        });
      }
      queryParamsObj.query += whereClause;

      this.getWhereClause(
        req.query._where,
        req.app.locals._tableName,
        queryParamsObj,
        " and "
      );
    } else if (listType === 3) {
      //bulkRead

      // select * from table where pk in (ids) and whereConditions
      queryParamsObj.params.push(req.app.locals._tableName);
      queryParamsObj.query += " where ?? in ";
      queryParamsObj.params.push(
        this.getPrimaryKeyName(req.app.locals._tableName)
      );

      queryParamsObj.query += "(";

      if (req.query && req.query._ids) {
        let ids = req.query._ids.split(",");
        for (var i = 0; i < ids.length; ++i) {
          if (i) {
            queryParamsObj.query += ",";
          }
          queryParamsObj.query += "?";
          queryParamsObj.params.push(ids[i]);
        }
      }
      queryParamsObj.query += ") ";
      this.getWhereClause(
        req.query._where,
        req.app.locals._tableName,
        queryParamsObj,
        " and "
      );
    } else {
      queryParamsObj.params.push(req.app.locals._tableName);

      /**************** add where clause ****************/
      this.getWhereClause(
        req.query._where,
        req.app.locals._tableName,
        queryParamsObj,
        " where "
      );
    }

    this._getGrpByHavingOrderBy(req, req.app.locals._tableName, queryParamsObj);

    //console.log(queryParamsObj.query, queryParamsObj.params);
  }

  _joinTableNames(isSecondJoin, joinTables, index, queryParamsObj) {
    if (isSecondJoin) {
      /**
       * in second join - there will be ONE table and an ON condition
       *   if clause deals with this
       *
       */

      // add : join / left join / right join / full join / inner join
      queryParamsObj.query += this.getJoinType(joinTables[index]);
      queryParamsObj.query += " ?? as ?? ";

      // eg: tbl.tableName
      let tableNameAndAs = joinTables[index + 1].split(".");

      if (
        tableNameAndAs.length === 2 &&
        !(tableNameAndAs[1] in this.sqlConfig.ignoreTables)
      ) {
        queryParamsObj.params.push(tableNameAndAs[1]);
        queryParamsObj.params.push(tableNameAndAs[0]);
      } else {
        queryParamsObj.grammarErr = 1;
        console.log("there was no dot for tableName ", joinTables[index + 1]);
      }
    } else {
      /**
       * in first join - there will be TWO tables and an ON condition
       *    else clause deals with this
       */

      // first table
      queryParamsObj.query += " ?? as ?? ";
      // add : join / left join / right join / full join / inner join
      queryParamsObj.query += this.getJoinType(joinTables[index + 1]);
      // second table
      queryParamsObj.query += " ?? as ?? ";

      let tableNameAndAs = joinTables[index].split(".");
      if (
        tableNameAndAs.length === 2 &&
        !(tableNameAndAs[1] in this.sqlConfig.ignoreTables)
      ) {
        queryParamsObj.params.push(tableNameAndAs[1]);
        queryParamsObj.params.push(tableNameAndAs[0]);
      } else {
        queryParamsObj.grammarErr = 1;
        console.log("there was no dot for tableName ", joinTables[index]);
      }

      tableNameAndAs = [];
      tableNameAndAs = joinTables[index + 2].split(".");
      if (
        tableNameAndAs.length === 2 &&
        !(tableNameAndAs[1] in this.sqlConfig.ignoreTables)
      ) {
        queryParamsObj.params.push(tableNameAndAs[1]);
        queryParamsObj.params.push(tableNameAndAs[0]);
      } else {
        queryParamsObj.grammarErr = 1;
        console.log("there was no dot for tableName ", joinTables[index]);
      }
    }
  }

  prepareJoinQuery(req, res, queryParamsObj) {
    queryParamsObj.query = "SELECT ";
    queryParamsObj.grammarErr = 0;

    while (1) {
      /**************** START : get fields ****************/
      if (req.query._fields) {
        let fields = req.query._fields.split(",");

        // from _fields to - ??, ??, ?? [col1,col2,col3]
        for (var i = 0; i < fields.length && !queryParamsObj.grammarErr; ++i) {
          if (i) {
            queryParamsObj.query += ",";
          }
          queryParamsObj.query += " ?? ";
          queryParamsObj.params.push(fields[i]);
          let aliases = fields[i].split(".");
          if (aliases.length === 2) {
            queryParamsObj.query += "as " + aliases[0] + "_" + aliases[1];
            //console.log(queryParamsObj.query);
          } else {
            queryParamsObj.grammarErr = 1;
          }
        }
      } else {
        queryParamsObj.grammarErr = 1;
      }

      queryParamsObj.query += " from ";

      if (queryParamsObj.grammarErr) {
        break;
      }

      /**************** END : get fields ****************/

      /**************** START : get join + on ****************/
      let joinTables = req.query._join.split(",");
      if (joinTables.length < 3) {
        //console.log('grammar error ', joinTables.length);
        queryParamsObj.grammarErr = 1;
        break;
      }

      //console.log('jointables.length', joinTables);

      let onCondnCount = 0;

      for (
        let i = 0;
        i < joinTables.length - 1 && queryParamsObj.grammarErr === 0;
        i = i + 2
      ) {
        onCondnCount++;

        this._joinTableNames(i, joinTables, i, queryParamsObj);

        if (queryParamsObj.grammarErr) {
          console.log("failed at _joinTableNames", queryParamsObj);
          break;
        }

        //console.log('after join tables', queryParamsObj);

        let onCondn = "_on" + onCondnCount;
        let onCondnObj = {};
        if (onCondn in req.query) {
          //console.log(onCondn, req.query[onCondn]);
          onCondnObj = whereHelp.getConditionClause(req.query[onCondn], " on ");
          //console.log('onCondnObj', onCondnObj);
          queryParamsObj.query += " on " + onCondnObj.query;
          queryParamsObj.params = queryParamsObj.params.concat(
            onCondnObj.params
          );
        } else {
          queryParamsObj.grammarErr = 1;
          //console.log('No on condition: ', onCondn);
          break;
        }

        if (i === 0) {
          i = i + 1;
        }
      }
      /**************** END : get join + on ****************/

      if (queryParamsObj.grammarErr) {
        break;
      } else {
        this.getWhereClause(
          req.query._where,
          " ignore ",
          queryParamsObj,
          " where "
        );
        this._getGrpByHavingOrderBy(req, "ignore", queryParamsObj, 5);
        //console.log('after where',queryParamsObj);
      }

      break;
    }

    if (queryParamsObj.grammarErr) {
      queryParamsObj.query = "";
      queryParamsObj.params = [];
    }

    return queryParamsObj;
  }
}

//expose class
module.exports = Xsql;
